{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# densenet论文复现\n",
    "\n",
    "给出代码运行的log截图，并提供文档解释densenet模型的结构。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "    with tf.variable_scope(scope, 'DenseNet', [images, num_classes]):\n",
    "        with slim.arg_scope(bn_drp_scope(is_training=is_training,\n",
    "                                         keep_prob=dropout_keep_prob)) as ssc:\n",
    "            ##########################\n",
    "            # Put your code here.\n",
    "            ##########################\n",
    "            \n",
    "            # 原始图像大小为224*224*3\n",
    "            # 初始化卷积操作，输出feature map 为 112*112*48\n",
    "            end_point = 'Pre_Conv2d_7x7'\n",
    "            current = slim.conv2d(images, 2*growth, [7, 7], stride=2, padding='same', scope=end_point)\n",
    "            end_points[end_point] = current\n",
    "            # 最大池化，降空间尺寸， 输出feature map 为 56*56*48\n",
    "            end_point = 'Pre_Pool_3x3'\n",
    "            current = slim.max_pool2d(current, [3, 3], stride=2, padding='same', scope=end_point)\n",
    "            end_points[end_point] = current\n",
    "\n",
    "            # DenseBlock1 输出feature map 为 56*56\n",
    "            end_point = 'Block1'\n",
    "            current = block(current, 6, growth, scope=end_point)\n",
    "            end_points[end_point] = current\n",
    "            # Transition1 输出feature map 为 28*28\n",
    "            end_point = 'Trans1_Conv2d_1x1'\n",
    "            current = bn_act_conv_drp(current, growth, [1, 1], scope=end_point)\n",
    "            end_points[end_point] = current\n",
    "            end_point = 'Trans1_avgPool_2x2'\n",
    "            current = slim.avg_pool2d(current, [2, 2], stride=2, padding='same', scope=end_point)\n",
    "            end_points[end_point] = current\n",
    "\n",
    "            # DenseBlock2 输出feature map 为 28*28\n",
    "            end_point = 'Block2'\n",
    "            current =  block(current, 12, growth, scope=end_point) \n",
    "            end_points[end_point] = current\n",
    "            # Transition2 输出feature map 为 14*14\n",
    "            end_point = 'Trans2_Conv2d_1x1'                     \n",
    "            current = bn_act_conv_drp(current, growth, [1, 1], scope=end_point)\n",
    "            end_points[end_point] = current\n",
    "            end_point = 'Trans2_avgPool_2x2' \n",
    "            current = slim.avg_pool2d(current, [2, 2], stride=2, padding='same', scope=end_point) \n",
    "            end_points[end_point] = current\n",
    "\n",
    "            # DenseBlock3 输出feature map 为 14*14\n",
    "            end_point = 'Block3'\n",
    "            current =  block(current, 24, growth, scope=end_point) \n",
    "            end_points[end_point] = current\n",
    "            # Transition3 输出feature map 为 7*7\n",
    "            end_point = 'Trans3_Conv2d_1x1'\n",
    "            current = bn_act_conv_drp(current, growth, [1, 1], scope=end_point)\n",
    "            end_points[end_point] = current\n",
    "            end_point = 'Trans3_avgPool_2x2' \n",
    "            current = slim.avg_pool2d(current, [2, 2], stride=2, padding='same', scope=end_point) \n",
    "            end_points[end_point] = current\n",
    "\n",
    "            # DenseBlock4 输出feature map 为 7*7\n",
    "            end_point = 'Block4'\n",
    "            current =  block(current, 16, growth, scope=end_point) \n",
    "            end_points[end_point] = current\n",
    "            # 全局池化 kernel size 7*7\n",
    "            end_point = 'Global_Pool_7x7' \n",
    "            current = slim.avg_pool2d(current, [7, 7], padding='same', scope=end_point)  \n",
    "            end_points[end_point] = current\n",
    "\n",
    "            # flatten后做全连接\n",
    "            end_point = 'Pre_Flatten'\n",
    "            current =  slim.flatten(current, scope=end_point)\n",
    "            end_points[end_point] = current\n",
    "            end_point = 'FC_Logits'\n",
    "            logits = slim.fully_connected(current, num_classes, activation_fn=None, scope=end_point)\n",
    "            end_points[end_point] = logits\n",
    "\n",
    "            # softmax分类\n",
    "            end_points['Predictions'] = tf.nn.softmax(logits, name='Predictions')\n",
    "\n",
    "    return logits, end_points\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### densenet模型的结构\n",
    "\n",
    "- 首先是划分成多个密集链接的 denseblocks，其中每个小块的内部的 feature map 是不变的，每个小块的内部又有会分成很多个小的 function，每个 function 由三个操作组成，每个小 function 会循环执行多次，每次生成的结果都会拼接 (concatenate) 到以前生成的所有的结果后面，形成一个长长的数组（即在第三个维度上越来越厚）。拼接好的结果再作为下一个 function 的输入。每个 block 之间会用**一个 BN，1\\*1 的卷积，2\\*2 的池化**来缩减 feature-maps 的长和宽，再送到一个小块里面去。  \n",
    "- Denseblock，是由 bottleneck（一个 1\\*1 卷积层。注意，DenseNets 中的卷积层，除了开始的第一层卷积层，还包括了 BN 层和激活函数层，结构为 BN-Relu-conv），和一个 3\\*3 的卷积层交替循环出现，总结为：BN-Relu-conv1x1-BN-Relu-conv3x3 交替出现。每一个 denseblock 中的卷积层的输入，都是此层之前所有卷积层输出的 feature maps 在 channel 维度上经由拼接操作而来。每个 denseblock 不改变 feature map 的宽度和高度，只在 channel 数上进行增加。\n",
    "- Transition layer，则是由一个 1\\*1 卷积层和一个 2\\*2 平均池化层组成，连接方式是传统的“输出接输入”的单通道连接。主要是对 denseblock 产生的 feature map 进行 channel 上的压缩和宽、高上的降维，以防止多次循环拼接后使得 feature maps 过多过大，带来计算上的繁重压力。\n",
    "- bottleneck layer：降维\n",
    "- compression：同样也是降维, 一般设压缩率为 0.5，可以使 block 后的 map 减小一半，使模型更加简洁。\n",
    "\n",
    "#### 实现细节：\n",
    "实现的是 growth = 24 包含 4 个 denseblock 的 DenseNet-BC 网络，每个 block 层数严格按照论文上的的描述来做（包括两个 1\\*1 和 3\\*3 卷积），输入进入第一个 denseblock 前进行了一次 7\\*7 的卷积 (stride=2) 和一个池化层，4 个 denseblock间有 3 个 transition 层，第 4 个 denseblock 与分类器之间用了一个全连接层。 数据集用的是 quiz 的 tfrecord 格式文件。\n",
    "\n",
    "#### 关于 growth rate\n",
    "由于每个层的输入是所有之前层输出的连接，因此每个层的输出不需要像传统网络一样多。所以这里的这个 growth rate 是每个function 输出的 feature map 数，是一个超参数。这个参数可以控制我们的网络结构不至于太复杂，即每一个 denseblock 最终的输出 channel 数不会太多，提高计算性能。对一些简单的容易识别的图像分类任务，可以适当采用小一些的 growth rate（如 12、24、32），就可以达到较好的模型性能；对于略复杂的训练数据，想要进一步提升分类准确率，就要采用更大的 growth rate，如 48、64 或者更大。无疑，这也将会带来训练时间的增长，和可能的过拟合问题需要注意和权衡。  \n",
    "\n",
    "论文中提出的 growth rate 关联整个 densenet 中每个阶段的 feature map 通道数的变化（本题中为 24），通道数的变化以 growth rate 为一个单位，进行整数倍的增加，应用于所有进行通道数变换的操作，如最开始的初始化卷积操作使用 2\\*growth，block 内部的 bottleneck layer 使用 4\\*growth，block 内部的 3\\*3 卷积使用 1\\*growth，而 transition layer 中的卷积操作使用 1\\*growth。 growth rate 控制着整个网络的宽度（避免通道数增长太快），选择小的 growth rate 可以让网络变窄，计算的参数数量也会减少。  \n",
    "\n",
    "#### 关于稠密链接\n",
    "每一层的输出都会加上之前所有层的输出作为下一层的输入，这样的连接即为稠密链接。对于有 L 层的传统网络，一共有 L 个连接，那么对于 DenseNet，则有 (L+1)\\*L/2 个连接。假设我们有 32 层，那么第 32 层的输入就是前面 31 层所有的输出连接起来的，所以叫做稠密链接。\n",
    "\n",
    "#### 卷积和池化 padding 参数为 same 和 valid 的区别\n",
    "\"VALID\": 会删除最右边的列（或最底部的行）。\n",
    "![valid](hw11-images/valid.PNG)  \n",
    "  \n",
    "\"SAME\":试图左右均匀填充，但如果要添加的列数是奇数，它将在右侧添加额外的列，就像本例中的情况一样（垂直方向也一样：可能会在最下面添加额外为零的一行）。\n",
    "![same](hw11-images/same.PNG)\n",
    "\n",
    "#### 优点\n",
    "- 由于传统网络随着深度的加大，梯度在传播的时候就容易出现梯度消失的问题，那么对于 densenet 来讲，由于每一层的输出都参与到了后续所有层的输入，所以很有效的解决了梯度消失这个问题。\n",
    "\n",
    "- 大幅度减少了参数的个数"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "代码不作为评判标准，如果运行正确，则认为代码没有错误。\n",
    "\n",
    "### - 训练运行成功并有准确率输出 60分。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "![100000](hw11-images/densenet-100000.PNG)\n",
    "\n",
    "![densenet-total_loss](hw11-images/densenet-total_loss.PNG)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "因为 batch_size 设成 32 会出现 [Resource exhausted: OOM when allocating tensor…] 错误讯息，所以将 batch_size 改设为 **16**。  \n",
    "\n",
    "因为没有预训练的 checkpoint 可供使用（实际上有尝试使用经过 ImageNet 训练过的 Densenet-121 Checkpoint，但加载后出现错误，无法执行）, max_number_of_steps 调整到 **60000**，才使 Top5 达到 80% 以上, 最后总计训练十万次，前后共花费约**十七个小时**。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### - Top5召回率达到80%以上 10分。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "![densenet-evaluation-100000-emphasis](hw11-images/densenet-evaluation-100000-emphasis.PNG)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Top1(Accuracy) = 0.710729182   \n",
    "**Top5(Recall) = 0.896770835** > 80%  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### - 对跳接的理解 10分。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "跳层连接方法是对中间层输出特征图信息的探索，之前的 ResNets 和 Highway Networks 都曾使用，把前层的输出特征图信息直接传递到后面的一些层，可以有效地提高信息传递效率和信息复用效率。  \n",
    "\n",
    "Densenet 尝试把跳层连接用到极致，**每一层的输入都是由前面各层的输出合成的**, 每一层都尽可能地往前面的层跳跃。在传统的卷积神经网络中，如果你有 L 层，那么就会有 L 个连接，但是在 DenseNet 中，会有 **L(L+1)/2 个连接**。  \n",
    "\n",
    "为了进行特征复用，在跨层连接时使用的是在**特征维度上的 Concatenate 操作**，而**不是 Element-wise Addition 操作**。\n",
    "\n",
    "Concatenate 操作层只是特征拼接，不对特征做任何改变。因此，只需相邻层的一个“短接”，后续所有层都可以拥有到该层的“通路”，这些通路意味着**更佳的信息前传与梯度反传**。  \n",
    "\n",
    "Concatenate 操作层会使得 feature map “变厚”，因此即使我们使用更少的卷积参数，feature map 也不会因过小而产生信息瓶颈，这也是 Densenet 一定程度上参数更少的原因。  \n",
    "\n",
    "![densenet-structure-1](hw11-images/densenet-denseblocks.PNG)  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### - 对growth rate的理解 10分。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "![densenet-structure-1](hw11-images/densenet-structure-1.PNG)  \n",
    "\n",
    "growth rate (增长率)指的是**每个单元模块最后那个 3\\*3 的卷积核的数量，记为 k**。由于每个单元模块最后是以 Concatenate 的方式来进行连接的，所以每经过一个单元模块，下一层的特征维度就会增长 k。它的值越大意味着在网络中流通的信息也越大，相应地网络的能力也越强，但是整个模型的尺寸和计算量也会变大。论文作者在论文中使用了 k=32 和 k=48 两种设置。\n",
    "\n",
    "对 growth rate 的一种解释是**每个层都有到其他密集块特征映射的访问权限**，另一种解释是**将特征图看做网络的全局状态**，即每一层为自己增加 k 个特征图。**增长率调节每一层贡献给全局状态的新信息量**。全局状态一旦写入，就可以从网络的任何地方访问它，这与传统网络体系结构不同，我们不需要一层一层地复制它。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### - 解释为什么densenet的参数量和运算量都不大，但是显存消耗却特别大 10分。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "论文作者对这个问题亲自撰写过技术报告并提出原因与解释，可参考  \n",
    "1. https://blog.csdn.net/comway_Li/article/details/82055229？utm_source=blogxgwz2\n",
    "2. https://www.cnblogs.com/bonelee/p/9029138.html\n",
    "3. https://arxiv.org/pdf/1707.06990.pdf\n",
    "\n",
    "以下直接引用部分内容：\n",
    "\n",
    ">不少人跟我们反映过 DenseNet 在训练时对内存消耗非常厉害。这个问题其实是算法实现不优带来的。当前的深度学习框架对 DenseNet 的密集连接没有很好的支持，我们只能借助于**反复的拼接（Concatenation）操作**，将之前层的输出与当前层的输出拼接在一起，然后传给下一层。对于大多数框架（如 Torch 和 TensorFlow），每次拼接操作都会开辟新的内存来保存拼接后的特征。这样就导致**一个 L 层的网络，要消耗相当于 L(L+1)/2 层网络的内存（第 l 层的输出在内存里被存了 (L-l+1) 份）**。\n",
    "\n",
    ">解决这个问题的思路其实并不难，我们**只需要预先分配一块缓存，供网络中所有的拼接层（Concatenation Layer）共享使用，这样 DenseNet 对内存的消耗便从平方级别降到了线性级别**。**在梯度反传过程中，我们再把相应卷积层的输出复制到该缓存，就可以重构每一层的输入特征，进而计算梯度**。当然网络中由于 Batch Normalization 层的存在，实现起来还有一些需要注意的细节。为此我们专门写了一个技术报告（Memory-Efficient Implementation of DenseNets, https://arxiv.org/pdf/1707.06990.pdf）介绍如何提升 DenseNet 对内存的使用效率，同时还提供了 Torch, PyTorch, MxNet 以及 Caffe 的实现，代码参见：\n",
    "\n",
    ">Torch implementation: https://github.com/liuzhuang13/DenseNet/tree/master/models\n",
    "\n",
    ">PyTorch implementation: https://github.com/gpleiss/efficient_densenet_pytorch\n",
    "\n",
    ">MxNet implementation: https://github.com/taineleau/efficient_densenet_mxnet\n",
    "\n",
    ">Caffe implementation: https://github.com/Tongcheng/DN_Caffe\n",
    "\n",
    ">新的实现极大地减小了 DenseNet 在训练时对显存的消耗，比如论文中 190 层的 DenseNet 原来几乎占满了 4 块 12G 内存的 GPU，而优化过后的代码仅需要 9G 的显存，在单卡上就能训练。\n",
    "\n",
    ">另外就是网络在推理（或测试）的时候对内存的消耗，这个是我们在实际产品中（尤其是在移动设备上）部署深度学习模型时最关心的问题。不同于训练，一般神经网络的推理过程不需要一直保留每一层的输出，因此可以在每计算好一层的特征后便将前面层特征占用的内存释放掉。而 DenseNet 则需要始终保存所有前面层的输出。但考虑到 DenseNet 每一层产生的特征图很少，所以在推理的时候占用内存不会多于其他网络。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
